QEMU 虚拟机逃逸漏洞(CVE-2019-14378)漏洞分析

这是qemu在网络实现的时候的一个指针错误,当重组大量的ipv4分段数据包时会触发错误,这还是大牛通过代码审计发现的,厉害啊。

漏洞细节

qemu的网络有两部分
1、为虚拟机提供的虚拟网卡(比如PCI网卡)
2、与网络接口控制器交互的网络后端(就是将网络数据包给到主机网络)

默认情况下,QEMU将为guest虚拟机创建SLiRP用户网络后端和适当的虚拟网卡(例如e1000 PCI网卡)

而本漏洞是在SLiRP中的数据包重组中出现的错误。

数据包重组那就需要了解ip分片,ip层的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

分片在Flags那里,主要是低3个bit

1
2
3
4
Bit 0: 保留为, 必须为0
Bit 1: (DF) 0 = 分片, 1 = 不分片.
Bit 2: (MF) 0 =最后一个ip包, 1 = 后面还有分片
Fragment Offset: 13 bits

下面看下相关的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct mbuf {
/* header at beginning of each mbuf: */
struct mbuf *m_next; /* Linked list of mbufs */
struct mbuf *m_prev;
struct mbuf *m_nextpkt; /* Next packet in queue/record */
struct mbuf *m_prevpkt; /* Flags aren't used in the output queue */
int m_flags; /* Misc flags */

int m_size; /* Size of mbuf, from m_dat or m_ext */
struct socket *m_so;

char *m_data; /* Current location of data */
int m_len; /* Amount of data in this mbuf, from m_data */

...

char *m_ext;
/* start of dynamic buffer area, must be last element */
char m_dat[];
};

mbuf是储存接收到的ip层的信息。有两个buffer,一个是m_dat ,另一个是m_ext,他是m_dat不足以储存的时候,通过在堆上分配内存解决不足的问题

在nat转换的时候,如果传入的数据包是分片的,则应在编辑和重新传输之前重新组装它们。 这个重组由ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp)函数完成。 ip包含当前的IP数据包数据,fp是包含分段数据包的链表。

ip_reass执行以下操作:
1、如果第一个分配的fp为NULL,创建重组队列并将ip插入此队列。
2、检查片段是否与先前收到的片段重复,然后丢弃它。
3、如果收到所有分段的数据包,则重新组装它。 通过修改第一个数据包的头部为新的ip header。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* Take incoming datagram fragment and try to
* reassemble it into whole datagram. If a chain for
* reassembly of this datagram already exists, then it
* is given as fp; otherwise have to make a chain.
*/
static struct ip *ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp)
{

...
...

/*
* Reassembly is complete; concatenate fragments.
*/
q = fp->frag_link.next;
m = dtom(slirp, q);

q = (struct ipasfrag *)q->ipf_next;
while (q != (struct ipasfrag *)&fp->frag_link) {
struct mbuf *t = dtom(slirp, q);
q = (struct ipasfrag *)q->ipf_next;
m_cat(m, t);
}

/*
* Create header for new ip packet by
* modifying header of first packet;
* dequeue and discard fragment reassembly header.
* Make header visible.
*/
q = fp->frag_link.next;

/*
* If the fragments concatenated to an mbuf that's
* bigger than the total size of the fragment, then and
* m_ext buffer was alloced. But fp->ipq_next points to
* the old buffer (in the mbuf), so we must point ip
* into the new buffer.
*/
if (m->m_flags & M_EXT) {
int delta = (char *)q - m->m_dat;
q = (struct ipasfrag *)(m->m_ext + delta);
}

这个漏洞在于计算变量delta的值有问题,而上面这个代码假定了第一个分片数据包不会在external buffer中分配(m_ext)。
当分片数据在mbuf-> m_dat(q将在m_dat内)时,计算q-m-> dat有效(q是包含分片链表和数据包数据的结构)。

否则,如果分配了m_ext缓冲区,则q将位于external buffer ,那么delta的计算就是错误的。

q的数据结构是ipasfrag:就是有一个前向指针跟后向指针,以及包含了一个ip头

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Ip header, when holding a fragment.
*
* Note: ipf_link must be at same offset as frag_link above
*/
struct ipasfrag {
struct qlink ipf_link;
struct ip ipf_ip;
};

struct qlink {
void *next, *prev;
};

我们可以调试看看q的某个时刻的状态是怎样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gdb-peda$ p *q
$30 = {
ipf_link = {
next = 0x7f9e08084ed0,
prev = 0x7f9e0808487c
},
ipf_ip = {
ip_hl = 0x5,
ip_v = 0x4,
ip_tos = 0x0,
ip_len = 0x8,
ip_id = 0x7f3a,
ip_off = 0x8,
ip_ttl = 0x40,
ip_p = 0x1,
ip_sum = 0x95e3,
ip_src = {
s_addr = 0xf02000a
},
ip_dst = {
s_addr = 0x202000a
}
}
}

可以看到确实是两个ipasfrag前后指针还有ip头信息

我们继续调试运行到下面地方

我们查看下数据结构,可以看到确实此时的q在m_ext的后面,而m_dat在老前面了,那么q - m->m_dat就是负数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
gdb-peda$ p q
$41 = (struct ipasfrag *) 0x7f9e0808882c
gdb-peda$ p *m
$42 = {
m_next = 0x7f9e080881a0,
m_prev = 0x7f9e080874c0,
m_nextpkt = 0x0,
m_prevpkt = 0x0,
m_flags = 0xd,
m_size = 0xcde,
m_so = 0x0,
m_data = 0x7f9e08088850 "",
m_len = 0xc98,
slirp = 0x563aa67a6380,
resolution_requested = 0x0,
expiration_date = 0xffffffffffffffff,
m_ext = 0x7f9e08088810 "",
m_dat = 0x7f9e08086eb0 ""
}
gdb-peda$ p *q
$43 = {
ipf_link = {
next = 0x7f9e0808487c,
prev = 0x7f9e08087520
},
ipf_ip = {
ip_hl = 0x5,
ip_v = 0x4,
ip_tos = 0x1,
ip_len = 0xc90,
ip_id = 0x7f3e,
ip_off = 0x0,
ip_ttl = 0x40,
ip_p = 0x1,
ip_sum = 0x1c43,
ip_src = {
s_addr = 0xffffff8b
},
ip_dst = {
s_addr = 0x0
}
}
}

简单的示意图如下:(忽略了分配了m_ext缓冲区,则q将位于external buffer)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+------------------------------+
| |
| |
| |
| |
| m_dat 0x7f9e08086eb0 |
| |
| |
+------------------------------+
| |
|m->m_ext 0x7f9e08088810 |
| |
| |
|q 0x7f9e0808882c |
| |
| |
| |
+------------------------------+

之后,新计算的指针q被转换为ip结构并且修改部分字段。由于错误地计算了delta,ip将指向不正确的位置,并且ip_src和ip_dst可用于将我们可控的数据写入错误计算的ip的位置。 如果计算出的ip位于没有映射的内存区域,这就会使qemu崩溃。

1
2
3
4
5
6
slirp/src/ip_input.c:ip_reass
ip = fragtoip(q); //转换
ip->ip_len = next;
ip->ip_tos &= ~1;
ip->ip_src = fp->ipq_src;
ip->ip_dst = fp->ipq_dst;

参考

https://blog.bi0s.in/2019/08/24/Pwn/VM-Escape/2019-07-29-qemu-vm-escape-cve-2019-14378/

打赏专区